// ==========================================================
// SGI Loader
//
// Design and implementation by
// - Sherman Wilcox
// - Noam Gat
//
// References :
// ------------
// - The SGI Image File Format, Version 1.0
// http://astronomy.swin.edu.au/~pbourke/dataformats/sgirgb/sgiversion.html
// - SGI RGB Image Format
// http://astronomy.swin.edu.au/~pbourke/dataformats/sgirgb/
//
//
// This file is part of FreeImage 3
//
// COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES
// THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE
// OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED
// CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT
// THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY
// SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL
// PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER
// THIS DISCLAIMER.
//
// Use at your own risk!
// ==========================================================

#include "FreeImage.h"
#include "Utilities.h"

// ----------------------------------------------------------
//   Constants + headers
// ----------------------------------------------------------

#ifdef _WIN32
#pragma pack(push, 1)
#else
#pragma pack(1)
#endif

typedef struct tagSGIHeader {
    /** IRIS image file magic number. This should be decimal 474. */
    WORD magic;
    /** Storage format: 0 for uncompressed, 1 for RLE compression. */
    BYTE storage;
    /** Number of bytes per pixel channel. Legally 1 or 2. */
    BYTE bpc;
    /**
       Number of dimensions. Legally 1, 2, or 3.
       1 means a single row, XSIZE long
       2 means a single 2D image
       3 means multiple 2D images
    */
    WORD dimension;
    /** X size in pixels */
    WORD xsize;
    /** Y size in pixels */
    WORD ysize;
    /**
       Number of channels.
       1 indicates greyscale
       3 indicates RGB
       4 indicates RGB and Alpha
    */
    WORD zsize;
    /** Minimum pixel value. This is the lowest pixel value in the image.*/
    LONG pixmin;
    /** Maximum pixel value. This is the highest pixel value in the image.*/
    LONG pixmax;
    /** Ignored. Normally set to 0. */
    char dummy[4];
    /** Image name. Must be null terminated, therefore at most 79 bytes. */
    char imagename[80];
    /**
       Colormap ID.
       0 - normal mode
       1 - dithered, 3 mits for red and green, 2 for blue, obsolete
       2 - index colour, obsolete
       3 - not an image but a colourmap
    */
    LONG colormap;
    /** Ignored. Should be set to 0, makes the header 512 bytes. */
    char reserved[404];
} SGIHeader;

typedef struct tagRLEStatus {
    int cnt;
    int val;
} RLEStatus;

#ifdef _WIN32
#pragma pack(pop)
#else
#pragma pack()
#endif

static const char *SGI_LESS_THAN_HEADER_LENGTH = "Incorrect header size";
static const char *SGI_16_BIT_COMPONENTS_NOT_SUPPORTED = "No 16 bit support";
static const char *SGI_COLORMAPS_NOT_SUPPORTED = "No colormap support";
static const char *SGI_EOF_IN_RLE_INDEX = "EOF in run length encoding";
static const char *SGI_EOF_IN_IMAGE_DATA = "EOF in image data";
static const char *SGI_INVALID_CHANNEL_COUNT = "Invalid channel count";

// ==========================================================
// Plugin Interface
// ==========================================================

static int s_format_id;

// ==========================================================
// Plugin Implementation
// ==========================================================

#ifndef FREEIMAGE_BIGENDIAN
static void
SwapHeader(SGIHeader *header) {
    SwapShort(&header->magic);
    SwapShort(&header->dimension);
    SwapShort(&header->xsize);
    SwapShort(&header->ysize);
    SwapShort(&header->zsize);
    SwapLong((DWORD*)&header->pixmin);
    SwapLong((DWORD*)&header->pixmax);
    SwapLong((DWORD*)&header->colormap);
}
#endif

static int
get_rlechar(FreeImageIO *io, fi_handle handle, RLEStatus *pstatus) {
    if (!pstatus->cnt) {
        int cnt = 0;
        while (0 == cnt) {
            BYTE packed = 0;
            if(io->read_proc(&packed, sizeof(BYTE), 1, handle) < 1) {
                return EOF;
            }
            cnt = packed;
        }
        if (cnt == EOF) {
            return EOF;
        }
        pstatus->cnt = cnt & 0x7F;
        if (cnt & 0x80) {
            pstatus->val = -1;
        } else {
            BYTE packed = 0;
            if(io->read_proc(&packed, sizeof(BYTE), 1, handle) < 1) {
                return EOF;
            }
            pstatus->val = packed;
        }
    }
    pstatus->cnt--;
    if (pstatus->val == -1) {
        BYTE packed = 0;
        if(io->read_proc(&packed, sizeof(BYTE), 1, handle) < 1) {
            return EOF;
        }
        return packed;
    }
    else {
        return pstatus->val;
    }
}

static const char * DLL_CALLCONV
Format() {
    return "SGI";
}

static const char * DLL_CALLCONV
Description() {
    return "SGI Image Format";
}

static const char * DLL_CALLCONV
Extension() {
    return "sgi,rgb,rgba,bw";
}

static const char * DLL_CALLCONV
RegExpr() {
    return NULL;
}

static const char * DLL_CALLCONV
MimeType() {
    return "image/x-sgi";
}

static BOOL DLL_CALLCONV
Validate(FreeImageIO *io, fi_handle handle) {
    BYTE sgi_signature[2] = { 0x01, 0xDA };
    BYTE signature[2] = { 0, 0 };

    io->read_proc(signature, 1, sizeof(sgi_signature), handle);

    return (memcmp(sgi_signature, signature, sizeof(sgi_signature)) == 0);
}

static BOOL DLL_CALLCONV
SupportsExportDepth(int depth) {
    return FALSE;
}

static BOOL DLL_CALLCONV
SupportsExportType(FREE_IMAGE_TYPE type) {
    return FALSE;
}

static FIBITMAP * DLL_CALLCONV
Load(FreeImageIO *io, fi_handle handle, int page, int flags, void *data) {
    int width = 0, height = 0, zsize = 0;
    int i, dim;
    int bitcount;
    SGIHeader sgiHeader;
    RLEStatus my_rle_status;
    FIBITMAP *dib = NULL;
    LONG *pRowIndex = NULL;

    try {
        // read the header
        memset(&sgiHeader, 0, sizeof(SGIHeader));
        if(io->read_proc(&sgiHeader, 1, sizeof(SGIHeader), handle) < sizeof(SGIHeader)) {
            throw SGI_LESS_THAN_HEADER_LENGTH;
        }
#ifndef FREEIMAGE_BIGENDIAN
        SwapHeader(&sgiHeader);
#endif
        if(sgiHeader.magic != 474) {
            throw FI_MSG_ERROR_MAGIC_NUMBER;
        }

        BOOL bIsRLE = (sgiHeader.storage == 1) ? TRUE : FALSE;

        // check for unsupported image types
        if (sgiHeader.bpc != 1) {
            // Expected one byte per color component
            throw SGI_16_BIT_COMPONENTS_NOT_SUPPORTED;
        }
        if (sgiHeader.colormap != 0) {
            // Indexed or dithered images not supported
            throw SGI_COLORMAPS_NOT_SUPPORTED;
        }

        // get the width & height
        dim = sgiHeader.dimension;
        width = sgiHeader.xsize;
        if (dim < 3) {
            zsize = 1;
        } else {
            zsize = sgiHeader.zsize;
        }

        if (dim < 2) {
            height = 1;
        } else {
            height = sgiHeader.ysize;
        }

        if(bIsRLE) {
            // read the Offset Tables
            int index_len = height * zsize;
            pRowIndex = (LONG*)malloc(index_len * sizeof(LONG));
            if(!pRowIndex) {
                throw FI_MSG_ERROR_MEMORY;
            }

            if ((unsigned)index_len != io->read_proc(pRowIndex, sizeof(LONG), index_len, handle)) {
                throw SGI_EOF_IN_RLE_INDEX;
            }

#ifndef FREEIMAGE_BIGENDIAN
            // Fix byte order in index
            for (i = 0; i < index_len; i++) {
                SwapLong((DWORD*)&pRowIndex[i]);
            }
#endif
            // Discard row size index
            for (i = 0; i < (int)(index_len * sizeof(LONG)); i++) {
                BYTE packed = 0;
                if( io->read_proc(&packed, sizeof(BYTE), 1, handle) < 1 ) {
                    throw SGI_EOF_IN_RLE_INDEX;
                }
            }
        }

        switch(zsize) {
            case 1:
                bitcount = 8;
                break;
            case 2:
                //Grayscale+Alpha. Need to fake RGBA
                bitcount = 32;
                break;
            case 3:
                bitcount = 24;
                break;
            case 4:
                bitcount = 32;
                break;
            default:
                throw SGI_INVALID_CHANNEL_COUNT;
        }

        dib = FreeImage_Allocate(width, height, bitcount);
        if(!dib) {
            throw FI_MSG_ERROR_DIB_MEMORY;
        }

        if (bitcount == 8) {
            // 8-bit SGI files are grayscale images, so we'll generate
            // a grayscale palette.
            RGBQUAD *pclrs = FreeImage_GetPalette(dib);
            for (i = 0; i < 256; i++) {
                pclrs[i].rgbRed = (BYTE)i;
                pclrs[i].rgbGreen = (BYTE)i;
                pclrs[i].rgbBlue = (BYTE)i;
                pclrs[i].rgbReserved = 0;
            }
        }

        // decode the image

        memset(&my_rle_status, 0, sizeof(RLEStatus));

        int ns = FreeImage_GetPitch(dib);
        BYTE *pStartRow = FreeImage_GetScanLine(dib, 0);
        int offset_table[] = { 2, 1, 0, 3 };
        int numChannels = zsize;
        if (zsize < 3) {
            offset_table[0] = 0;
        }
        if (zsize == 2)
        {
            //This is how faked grayscale+alpha works.
            //First channel goes into first
            //second channel goes into alpha (4th channel)
            //Two channels are left empty and will be copied later
            offset_table[1] = 3;
            numChannels = 4;
        }

        LONG *pri = pRowIndex;
        for (i = 0; i < zsize; i++) {
            BYTE *pRow = pStartRow + offset_table[i];
            for (int j = 0; j < height; j++, pRow += ns, pri++) {
                BYTE *p = pRow;
                if (bIsRLE) {
                    my_rle_status.cnt = 0;
                    io->seek_proc(handle, *pri, SEEK_SET);
                }
                for (int k = 0; k < width; k++, p += numChannels) {
                    int ch;
                    BYTE packed = 0;
                    if (bIsRLE) {
                        ch = get_rlechar(io, handle, &my_rle_status);
                        packed = (BYTE)ch;
                    }
                    else {
                        ch = io->read_proc(&packed, sizeof(BYTE), 1, handle);
                    }
                    if (ch == EOF) {
                        throw SGI_EOF_IN_IMAGE_DATA;
                    }
                    *p = packed;
                }
            }
        }

        if (zsize == 2)
        {
            BYTE *pRow = pStartRow;
            //If faking RGBA from grayscale + alpha, copy first channel to second and third
            for (int i=0; i<height; i++, pRow += ns)
            {
                BYTE *pPixel = pRow;
                for (int j=0; j<width; j++)
                {
                    pPixel[2] = pPixel[1] = pPixel[0];
                    pPixel += 4;
                }
            }
        }
        if(pRowIndex)
            free(pRowIndex);

        return dib;

    } catch(const char *text) {
        if(pRowIndex) free(pRowIndex);
        if(dib) FreeImage_Unload(dib);
        FreeImage_OutputMessageProc(s_format_id, text);
        return NULL;
    }
}

// ==========================================================
//   Init
// ==========================================================

void DLL_CALLCONV
InitSGI(Plugin *plugin, int format_id) {
    s_format_id = format_id;

    plugin->format_proc = Format;
    plugin->description_proc = Description;
    plugin->extension_proc = Extension;
    plugin->regexpr_proc = RegExpr;
    plugin->open_proc = NULL;
    plugin->close_proc = NULL;
    plugin->pagecount_proc = NULL;
    plugin->pagecapability_proc = NULL;
    plugin->load_proc = Load;
    plugin->save_proc = NULL;
    plugin->validate_proc = Validate;
    plugin->mime_proc = MimeType;
    plugin->supports_export_bpp_proc = SupportsExportDepth;
    plugin->supports_export_type_proc = SupportsExportType;
    plugin->supports_icc_profiles_proc = NULL;
}
